[iOS] マクロでパターンマッチ的なものを作る | アドカレ2013 : SP #22
今回の記事は AdventCalendar ということもあり、実用的な内容ではなくちょっとしたお遊びです。
マクロ
Objective-C では C のマクロを利用することができます。これを利用してシンタックスシュガーっぽいものを作ってみたいと思います。
パターンマッチ
今回は、パターンマッチのようなものを無理矢理作ります。さすがに汎用的に作るのはハードルが高いので、今回は比較的簡単な Maybe のみを処理できるものにします。
満たすべき条件は下記の通りとします。
- 式であること
- Just が内包する値を束縛できること
- コンパイラによって網羅性チェックが行われること
なお、Maybe は、値を返さないかもしれない値を表すために Haskell で使われている型です。Scala では同様のものが Option 型として標準ライブラリで提供されています。Foundation.framework ではこれに相当するものが提供されていませんので、別途定義する必要があります。
Maybe
ではまずは、Maybe 型を定義します。ソースコードは下記の通りです。
Maybe.h
@interface Maybe : NSObject - (id)valueFromJust; @end @interface Just : Maybe + (instancetype)justWithValue:(id)value; - (instancetype)initWithValue:(id)value; @end @interface Nothing : Maybe + (instancetype)nothing; @end
Maybe.m
@implementation Maybe - (id)init { if ([self class] == [Maybe class]) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Maybe is abstract class" userInfo:nil]; } self = [super init]; return self; } - (id)valueFromJust { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%s must be overriden in subclasses.", __PRETTY_FUNCTION__] userInfo:nil]; } @end @interface Just () @property (nonatomic, readonly) id value; @end @implementation Just + (instancetype)justWithValue:(id)value { return [[self alloc] initWithValue:value]; } - (instancetype)initWithValue:(id)value { self = [super init]; if (self) { _value = [value copy]; } return self; } - (id)init { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"You must not use %s. Use +[Just justWithValue:] instead.", __PRETTY_FUNCTION__] userInfo:nil]; } - (id)valueFromJust { return self.value; } @end @implementation Nothing + (instancetype)nothing { static Nothing *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[Nothing alloc] initInternal]; }); return sharedInstance; } - (id)init { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"You must not use %s. Use +[Nothing nothing] instead.", __PRETTY_FUNCTION__] userInfo:nil]; } - (instancetype)initInternal { self = [super init]; if (self) { } return self; } - (id)valueFromJust { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Nothing does not contain any value." userInfo:nil]; } @end
Maybe 型は Just 型と Nothing 型を元とする直和型です。Objective-C には直和型を定義する構文が enum 以外ありませんが、enum では元が直積型である型は定義できません。そこで、Maybe 型の定義は Scala の Option 型のように各元をサブクラスとする方法にしました。
Just が内包する値は指定イニシャライザ initWithValue: で id 型のパラメータとして渡します。Nothing はコンビニエンスコンストラクタ nothing を定義し、Scala の None 型と同じくシングルトンにしています。
また、内包する値を取り出すためのメソッドとして、Maybe にメソッド valueFromJust を定義しています。Just では内包する値をそのまま返すよう、Nothing では例外をスローするようオーバーライドしています。
なお、今回は Maybe の合成については目的外ですので、モナドとしての機能に関する実装はしません。
テストを書く
マクロを定義するにあたって、テストコードを書いて正常に動作しているか確認しながら進めていきます。テストコードは、下記の通りです。
- (void)testMatchMaybeJust { Maybe *maybe = [Just justWithValue:@"Hello"]; // -- パターンマッチで実現したい処理 -- id obj = nil; if ([maybe isMemberOfClass:[Just class]]) { obj = [[maybe valueFromJust] stringByAppendingString:@", world!"]; } else if ([maybe isMemberOfClass:[Nothing class]]) { obj = @"empty"; } // ---- XCTAssertNotNil(obj, @"obj should not be nil"); XCTAssertEqualObjects(obj, @"Hello, world!", @"obj should be a string representing 'Hello, world!'"); } - (void)testMatchMaybeNothing { Maybe *maybe = [Nothing nothing]; // -- パターンマッチで実現したい処理 -- id obj = nil; if ([maybe isMemberOfClass:[Just class]]) { obj = [[maybe valueFromJust] stringByAppendingString:@", world!"]; } else if ([maybe isMemberOfClass:[Nothing class]]) { obj = @"empty"; } // ---- XCTAssertNotNil(obj, @"obj should not be nil"); XCTAssertEqualObjects(obj, @"empty", @"obj should be a string representing 'empty'"); }
このテストでは、Maybe でくるまれる値の型は NSString 型であることを前提とします。Objective-C では型パラメータを取る型を扱うことができないので、内包する値の型に関してはプログラマ側で注意する必要があります。
Just 型のインスタンスが渡された場合は、内包する NSString 型の値を取り出して、文字列 ", world!" を連結した文字列を計算結果として扱います。一方、Nothing 型のインスタンスが渡された場合は、文字列 "empty" を計算結果として扱います。
最終的には上記処理を、パターンマッチのような見た目のマクロで置き換えられるようにします。
網羅性チェック
switch 文と列挙型
Xcode で利用されている Apple LLVM Compiler は、switch 文で列挙型を処理すると default 節を書いていない場合に、全ての列挙型のメンバが case 節で処理されているかチェックします。case 節で処理されていないメンバがある場合にはコンパイル時に警告を出力します。これをうまく利用して網羅性チェックを実現しつつ、Just 型と Nothing 型の処理分岐をしたいと思います。
MaybeType 列挙型
Objective-C の switch 文はオブジェクトを処理することができません。よって、Just と Nothing を表す列挙型を定義し、それを Maybe 型のメンバとして持たせます。その上で、switch 文ではその列挙型を処理することにします。
列挙型の定義は下記の通りです。
Maybe.h
typedef NS_ENUM(NSInteger, MaybeType) { MaybeTypeJust, MaybeTypeNothing };
定義した列挙型を Maybe 型のメンバにします。Type カテゴリを定義して、そこに MaybeType 型のプロパティを追加します。
Maybe.h
@interface Maybe (Type) @property (nonatomic, readonly) MaybeType type; @end
Maybe.m
@implementation Just (Type) - (MaybeType)type { return MaybeTypeJust; } @end @implementation Nothing (Type) - (MaybeType)type { return MaybeTypeNothing; } @end
この状態で、テストコードを修正します。
- (void)testMatchMaybeJust { Maybe *maybe = [Just justWithValue:@"Hello"]; id obj = nil; switch (maybe.type) { case MaybeTypeJust: obj = [[maybe valueFromJust] stringByAppendingString:@", world!"]; break; case MaybeTypeNothing: obj = @"empty"; break; } XCTAssertNotNil(obj, @"obj should not be nil"); XCTAssertEqualObjects(obj, @"Hello, world!", @"obj should be a string representing 'Hello, world!'"); }
Nothing のテストコードも同様に修正します。
上記コードの case 節がどちらか一方でも欠けている場合には、コンパイラが網羅性チェックの警告を出します。これは、マクロを定義してから確認します。
シンタックスシュガーを作るマクロを定義する
では、パターンマッチのようなシンタックスシュガーを提供するマクロを定義します。
Maybe.h
#define match_maybe(MAYBE) \ Maybe *INNER_MAYBE = MAYBE; \ switch (INNER_MAYBE.type) #define just(...) MaybeTypeJust: { \ __VA_ARGS__ \ } \ break; #define nothing(...) MaybeTypeNothing: { \ __VA_ARGS__ \ } \ break;
このマクロを使うと、テストコードは下記の様に変更することができます。
- (void)testMatchMaybeJust { Maybe *maybe = [Just justWithValue:@"Hello"]; id obj = nil; match_maybe (maybe) { case just ({ obj = [[maybe valueFromJust] stringByAppendingString:@", world!"]; }); case nothing ({ obj = @"empty"; }); } XCTAssertNotNil(obj, @"obj should not be nil"); XCTAssertEqualObjects(obj, @"Hello, world!", @"obj should be a string representing 'Hello, world!'"); }
なんとも不格好な構文になってきましたが、気にしません。
match_maybe マクロは、switch 文に Maybe 型インスタンスの type プロパティの値を渡す処理を、あたかも Maybe 型そのものを渡しているように見せかけています。
また、just マクロと nothing マクロを併用することによって、コード上から MaybeType 型の値と break が消えました。これらのマクロでは、可変長引数を取ることを宣言するとステートメントを渡すことができることを利用しています。
なお、match_maybe マクロで一度 Maybe 型の変数に代入し直しているのは、Maybe 型以外のインスタンスが渡された際にコンパイラに警告を出させるためです。例えば、NSObject 型のインスタンスを渡すと下の様に警告が出ます。
網羅性チェックの確認
では、マクロを定義したので、改めて網羅性チェックの確認をします。
just マクロの case 節をコメントアウトすると、下の様に MaybeTypeJust に関する警告が出ます。
nothing マクロの case 節をコメントアウトすると、下の様に MaybeTypeNothing に関する警告が出ます。
コメントアウトを外すと、警告が消えます。
式と内包する値の束縛
先程定義した match_maybe マクロでは、計算結果を外部で宣言された id 型の obj に代入しており、match_maybe 自体は値を返していません。これを値を返すように振る舞わせます。
マクロを下記のように修正します。
#define match_maybe(MAYBE, ...) ^id{ \ Maybe *INNER_MAYBE = MAYBE; \ id INNER_RESULT = nil; \ switch (INNER_MAYBE.type) \ __VA_ARGS__ \ return INNER_RESULT; \ }(); #define just(BLOCK) \ MaybeTypeJust: { \ id (^INNER_BLOCK)(id) = BLOCK; \ id INNER_VALUE = [INNER_MAYBE valueFromJust]; \ INNER_RESULT = INNER_BLOCK(INNER_VALUE); \ break; \ } \ #define nothing(BLOCK) \ MaybeTypeNothing: { \ id (^INNER_BLOCK)(void) = BLOCK; \ INNER_RESULT = INNER_BLOCK(); \ break; \ } \
テストコードは下記の様に変更します。
- (void)testMatchMaybeJust { Maybe *maybe = [Just justWithValue:@"Hello"]; id obj = match_maybe (maybe, { case just (^id(id value) { return [value stringByAppendingString:@", world!"]; }); case nothing (^id{ return @"empty"; }); }); XCTAssertNotNil(obj, @"obj should not be nil"); XCTAssertEqualObjects(obj, @"Hello, world!", @"obj should be a string representing 'Hello, world!'"); }
さらに不格好な構文になりましたが、気にしません。
式
match_maybe マクロでは、パターンマッチにおける計算結果の値を返却できるよう、処理を Blocks によって定義される無名関数の内部で行い、その無名関数を即実行しています。このパターンは、JavaScript では即時関数という名前で呼ばれています。JavaScript では関数を利用することでしかスコープを作ることができないため、スコープを制御したい場合によく利用されています。
なお、今回の例においても、マクロ内部で宣言している変数が Blocks の作るスコープ内で閉じているので、外部からアクセスできません。また、Blocks 内部から外部スコープの変数を書き換えるためには、対象の変数を __blocks 修飾子をつけて宣言する必要があります。これにより、マクロ外部で宣言されていた変数の値をマクロ内部で上書きしてしまうような事故を防ぐ効果も得られています。
少しイメージしづらいので、マクロを展開したコードを載せておきます。
id obj = ^id{ Maybe *INNER_MAYBE = maybe; id INNER_RESULT = nil; switch (INNER_MAYBE.type) { case MaybeTypeJust: { id (^INNER_BLOCK)(id) = ^id(id value) { return [value stringByAppendingString:@", world!"]; }; id INNER_VALUE = [INNER_MAYBE valueFromJust]; INNER_RESULT = INNER_BLOCK(INNER_VALUE); break; } case MaybeTypeNothing: { id (^INNER_BLOCK)(void) = ^id{ return @"empty"; }; INNER_RESULT = INNER_BLOCK(); break; } } return INNER_RESULT; }();
内包する値の束縛
修正前の just マクロにおいては、引数として渡しているブロック(Blocks ではありません。新たなスコープを作る C 言語のブロックです。)では、Just 型のインスタンスから valueFromJust メソッドを使って値を取り出しています。修正後の just マクロではこの処理を記述することなく、あらかじめ Just 型のインスタンスが内包する値を id 型の value 変数に束縛されています。
これで、パターンマッチのシンタックスシュガーっぽいものができました。かなり無理がありますが。
マクロ補足
マクロ名の先頭に@をつける
マクロ名の先頭に @ をつけると、まるで Objective-C の組み込み構文のように見せることができます。ただし、通常マクロの名前に @ を使うことはできません。ただし、下記コードの方法で、マクロ名の先頭に @ がついているように見せかけることができます。
#define loop \ autoreleasepool {} \ while (true) @loop { // break するまでループする }
これは、先頭に @ がついている組み込み構文をマクロ定義の先頭で @ なしで利用することで実現しています。今回定義したマクロは値を返す必要があるので、残念ながらこの方法は利用できません。
可変長引数の扱いについて
マクロの可変長引数は、 __VA_ARGS__ という変数に代入されます。しかし、正攻法では特定の要素を取り出すことはおろか、この中にある要素の数すら取得することすらできません。そこで便利なのが、OSS ライブラリ libextobjc の中にある metamacros.h です。ここには、__VA_ARGS__ を操作するための強力なマクロが定義されています。これを利用すると実現出来ることがかなり広がりそうです。
まとめ
マクロはなかなか強力なので、色々いじって遊ぶと面白いですね。
ただし、マクロの乱用はソースコードの可読性を著しく低くする可能性が高いので注意して下さい。アプリケーションコードにはなるべく使わないよう心がけた方がいいかと思います。